package resml

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// 操作系统的文件对象存储
type OsFileObjectStorage struct {
	root      string
	metapath  string // 元数据路径：元数据的 Path（就是文件对象的路径），元数据中的 Etag 映射到资源内容的路径
	objspath  string // 资源内容的储存路径
	refspath  string // 资源内容的引用路径，如果无引用，由 removeobj 指定是否删除资源内容
	temppath  string // 临时文件路径
	removeobj bool   // 当资源内容没有关联的元数据时，是否删除资源内容
	etager    ETager
	states    *StorageStates
}

// 创建操作系统的文件存储，以 root 为根目录。
func NewOsFileObjectStorage(root string, removeIfNoRef bool, etager ETager) (s *OsFileObjectStorage, err error) {
	root, err = filepath.Abs(filepath.FromSlash(filepath.Clean(root)))
	if err != nil {
		return
	}
	if err = os.MkdirAll(root, 0755); err != nil {
		return
	}
	if etager == nil {
		etager = NewSha256HexEtager()
	}
	return &OsFileObjectStorage{
		root:      root,
		metapath:  filepath.Join(root, "meta"),
		objspath:  filepath.Join(root, "objs"),
		refspath:  filepath.Join(root, "refs"),
		temppath:  filepath.Join(root, "temp"),
		removeobj: removeIfNoRef,
		etager:    etager,
		states:    NewStorageStates(),
	}, nil
}

// 获取资源
func (s *OsFileObjectStorage) Get(path string) (r io.ReadCloser, meta Meta, err error) {
	meta, err = s.getMeta(path)
	if err != nil {
		return
	}
	r, err = os.Open(s.objectPath(meta))
	return
}

// 获取资源的元数据
func (s *OsFileObjectStorage) Meta(path string) (meta Meta, err error) {
	return s.getMeta(path)
}

// 获取资源列表
func (s *OsFileObjectStorage) List(path string) (*StorageInfo, error) {
	fullpath := s.metaPath(path)
	stat, err := os.Stat(fullpath)
	if err != nil {
		return nil, err
	}
	respath := strings.Trim(filepath.ToSlash(filepath.Clean(path)), "/")
	si := &StorageInfo{Name: respath, IsContainer: stat.IsDir(), Size: stat.Size(), ModTime: stat.ModTime()}
	if !si.IsContainer {
		if meta, err := s.getMeta(path); err != nil {
			return nil, err
		} else { // 不是目录，则需要从元数据中获取真实的长度与修改时间
			si.Size, si.ModTime = meta.ContentLength(), meta.LastModified()
		}
		return si, nil
	}
	dirs, err := os.ReadDir(fullpath)
	if err != nil {
		return nil, err
	}
	si.Items = make([]*StorageInfo, 0, len(dirs))
	for _, de := range dirs {
		it := &StorageInfo{Name: strings.Trim(fmt.Sprintf("%s/%s", respath, de.Name()), "/"), IsContainer: de.IsDir()}
		if it.IsContainer {
			if ds, err := de.Info(); err == nil {
				it.Size, it.ModTime = ds.Size(), ds.ModTime()
			}
		} else if meta, err := s.getMeta(path); err == nil {
			it.Size, it.ModTime = meta.ContentLength(), meta.LastModified()
		}
		si.Items = append(si.Items, it)
	}
	return si, nil
}

// 对象的状态，如果状态不存在，则返回 nil
func (s *OsFileObjectStorage) State(path string) (*StorageState, error) {
	return s.states.State(s.metaPath(path)), nil
}

// 保存资源
func (s *OsFileObjectStorage) Save(path string, meta Meta, content io.Reader) error {
	oldmeta, err := s.getMeta(path)
	if err == ErrWriting {
		return err // 正在写入
	}
	s.states.WriteStarted(path)
	defer s.states.Writed(path) // 写入完成
	if meta == nil || meta.ETag() == nil || meta.ETag().Hash() == "" {
		return s.saveAndComputeETag(path, content)
	}
	if err == nil && meta.ContentLength() >= 0 {
		// meta 已存在，并且长度已知时
		if MetaEtagSizeEquals(oldmeta, meta) && s.objectExists(meta, true) {
			// 如果资源内容已存在，无需再存储内容
			if MetaEquals(oldmeta, meta) {
				return nil // meta 完全一样，无需更新
			}
			return s.saveMeta(path, meta) // 更新 meta
		}
		// 资源内容不存在（ETag或大小不同、资源内容文件缺失），先删除旧资源
		if err = s.remove(path, oldmeta); err != nil {
			return err
		}
	}
	// meta 不存在，但内容存在，只需要存储 meta
	if s.objectExists(meta, true) {
		if computed, err := MetaOfFileWithoutETag(s.objectPath(meta)); err == nil && computed != meta {
			return s.saveMeta(path, metaFix(meta, computed))
		} else {
			return err
		}
	}
	// 存储内容：meta.ETag 是存在的，存储并校验 ETag 以保证一致性
	fd, err := FileCreate(s.objectPath(meta), false)
	if err != nil {
		return err
	}
	defer fd.Close()
	etager, err := NewETager(meta.ETag().Method(), meta.ETag().Coder())
	if err != nil {
		return fmt.Errorf("%v, method %s, coder %s", err, meta.ETag().Method(), meta.ETag().Coder())
	}
	mtbuf := new(bytes.Buffer) // 缓存 512 字节用于计算 mime-type
	et, n, err := etager.OfCopy(io.MultiWriter(fd, NewLimitedWriter(mtbuf, 512)), content)
	if err != nil {
		return err
	}
	if !ETagEquals(et, meta.ETag()) {
		return fmt.Errorf("etag from reader [%s] is not equals to expected [%s]", et.String(), meta.ETag().String())
	}
	if meta.LastModified().IsZero() || meta.LastModified().UTC().IsZero() {
		meta.SetLastModified(time.Now())
	}
	if meta.ContentLength() <= 0 {
		meta.SetContentLength(n)
	}
	if n != meta.ContentLength() {
		return fmt.Errorf("size wrote from reader [%d] is not equals to expected [%d]", n, meta.ContentLength())
	}
	if meta.ContentType() == "" {
		meta.SetContentType(mimeTypeDetect(mtbuf.Bytes(), filepath.Ext(path)))
	}
	return s.saveMeta(path, meta) // 保存 meta
}

// 删除对象
func (s *OsFileObjectStorage) Remove(path string) error {
	meta, err := s.getMeta(path)
	if err != nil {
		return err
	}
	return s.remove(path, meta)
}

// 重命名/移动对象
func (s *OsFileObjectStorage) Rename(oldpath, newpath string) error {
	meta, err := s.getMeta(oldpath)
	if err != nil {
		return err
	}
	oldfn, newfn := s.metaPath(oldpath), s.metaPath(newpath)
	if err = os.Rename(oldfn, newfn); err != nil {
		return err
	}
	if err = s.refRemove(oldpath, meta); err != nil {
		os.Rename(newfn, oldfn)
		return err
	}
	if err = s.refSave(newpath, meta); err != nil {
		os.Rename(newfn, oldfn)
	}
	return err
}

// 保存并重新计算 Etag
func (s *OsFileObjectStorage) saveAndComputeETag(path string, content io.Reader) error {
	tfn := base64.RawURLEncoding.EncodeToString([]byte(path))
	tmp := filepath.Join(s.temppath, fmt.Sprintf("%s.%s", tfn, time.Now().Format("20060102150405")))
	if _, err := os.Stat(tmp); err == nil {
		return ErrWriting
	}
	fd, err := FileCreate(tmp, false)
	if err != nil {
		return err
	}
	meta, err := MetaOfCopy(fd, content, s.etager, time.Now(), filepath.Ext(path))
	fd.Close()
	if err != nil {
		return err
	}
	if err = os.Rename(tmp, s.objectPath(meta)); err != nil {
		return err
	}
	return s.saveMeta(path, meta)
}

// 根据元数据中的 Etag 计算对象的 Key
func (s *OsFileObjectStorage) objectKey(meta Meta) string {
	hv := meta.ETag().Hash()
	return filepath.Join(hv[:2], hv[2:4], meta.ETag().String())
}

// 根据元数据中的 Etag 生成资源的对象文件的路径
func (s *OsFileObjectStorage) objectPath(meta Meta) string {
	return filepath.Join(s.objspath, s.objectKey(meta))
}

// 元数据目录
func (s *OsFileObjectStorage) metaPath(path string) string {
	return filepath.Join(s.metapath, s.clean(path))
}

// 获取元数据
func (s *OsFileObjectStorage) getMeta(path string) (meta Meta, err error) {
	path = s.metaPath(path)
	if s.states.IsWriting(path) {
		return nil, ErrWriting
	}
	return MetaOfHttpHeaderTextFile(path)
}

// 保存元数据
func (s *OsFileObjectStorage) saveMeta(path string, meta Meta) error {
	path = s.clean(path)
	fd, err := FileCreate(s.metaPath(path), false)
	if err != nil {
		return err
	}
	defer fd.Close()
	fd.WriteString(MetaHttpHeaderText(meta))
	// 保存引用
	return s.refSave(path, meta)
}

// 根据元数据中的 Etag 生成资源的引用文件的路径
func (s *OsFileObjectStorage) refPath(meta Meta) string {
	return filepath.Join(s.refspath, s.objectKey(meta))
}

// 判断资源的对象文件是否存在
func (s *OsFileObjectStorage) objectExists(meta Meta, cmpSize bool) bool {
	stat, err := os.Stat(s.objectPath(meta))
	if err != nil || stat.IsDir() {
		return false
	}
	if meta.ContentLength() < 0 {
		meta.SetContentLength(stat.Size())
	}
	if cmpSize {
		return stat.Size() == meta.ContentLength()
	}
	return true
}

// 删除对象
func (s *OsFileObjectStorage) remove(path string, meta Meta) error {
	if err := FileRemove(s.metaPath(path), s.metapath); err != nil { // 删除元数据文件
		return err
	}
	if err := s.refRemove(s.clean(path), meta); err != nil { // 删除引用文件
		return err
	}
	if s.removeobj && DirIsEmpty(s.refPath(meta)) {
		return FileRemove(s.objectPath(meta), s.objspath)
	}
	return nil
}

// 清理路径
func (s *OsFileObjectStorage) clean(path string) string {
	return filepath.FromSlash(strings.Trim(filepath.Clean(path), "/\\"))
}

// 保存引用
func (s *OsFileObjectStorage) refSave(path string, meta Meta) error {
	rp := filepath.Join(s.refPath(meta), base64.RawURLEncoding.EncodeToString([]byte(path)))
	fd, err := FileCreate(rp, false)
	if err != nil {
		return err
	}
	defer fd.Close()
	_, err = fd.WriteString(path)
	return err
}

// 删除引用，并删除引用的空目录
func (s *OsFileObjectStorage) refRemove(path string, meta Meta) error {
	rp := filepath.Join(s.refPath(meta), base64.RawURLEncoding.EncodeToString([]byte(path)))
	return FileRemove(rp, s.refspath)
}
